/**
 * Copyright (C) 2008 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.inject.internal;

import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Binder;
import com.google.inject.Exposed;
import com.google.inject.Key;
import com.google.inject.PrivateBinder;
import com.google.inject.Provider;
import com.google.inject.Provides;
import com.google.inject.internal.BytecodeGen.Visibility;
import com.google.inject.internal.util.StackTraceElements;
import com.google.inject.spi.BindingTargetVisitor;
import com.google.inject.spi.Dependency;
import com.google.inject.spi.HasDependencies;
import com.google.inject.spi.ProviderInstanceBinding;
import com.google.inject.spi.ProviderWithExtensionVisitor;
import com.google.inject.spi.ProvidesMethodBinding;
import com.google.inject.spi.ProvidesMethodTargetVisitor;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Set;

/**
 * A provider that invokes a method and returns its result.
 *
 * @author jessewilson@google.com (Jesse Wilson)
 */
public abstract class ProviderMethod<T> implements ProviderWithExtensionVisitor<T>, HasDependencies, ProvidesMethodBinding<T> {

    /**
     * Creates a {@link ProviderMethod}.
     *
     * <p>Unless {@code skipFastClassGeneration} is set, this will use
     * {@link net.sf.cglib.reflect.FastClass} to invoke the actual method, since it is significantly
     * faster. However, this will fail if the method is {@code private} or {@code protected}, since
     * fastclass is subject to java access policies.
     */
    static <T> ProviderMethod<T> create(Key<T> key, Method method, Object instance, ImmutableSet<Dependency<?>> dependencies, List<Provider<?>> parameterProviders, Class<? extends Annotation> scopeAnnotation, boolean skipFastClassGeneration, Annotation annotation) {
        int modifiers = method.getModifiers();
        /*if[AOP]*/
        if (!skipFastClassGeneration && !Modifier.isPrivate(modifiers) && !Modifier.isProtected(modifiers)) {
            try {
                // We use an index instead of FastMethod to save a stack frame.
                return new FastClassProviderMethod<T>(key, method, instance, dependencies, parameterProviders, scopeAnnotation, annotation);
            } catch (net.sf.cglib.core.CodeGenerationException e) {
                /* fall-through */}
        }
        /*end[AOP]*/

        if (!Modifier.isPublic(modifiers) || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
            method.setAccessible(true);
        }

        return new ReflectionProviderMethod<T>(key, method, instance, dependencies, parameterProviders, scopeAnnotation, annotation);
    }

    protected final Object instance;
    protected final Method method;

    private final Key<T> key;
    private final Class<? extends Annotation> scopeAnnotation;
    private final ImmutableSet<Dependency<?>> dependencies;
    private final List<Provider<?>> parameterProviders;
    private final boolean exposed;
    private final Annotation annotation;

    /**
     * @param method the method to invoke. It's return type must be the same type as {@code key}.
     */
    private ProviderMethod(Key<T> key, Method method, Object instance, ImmutableSet<Dependency<?>> dependencies, List<Provider<?>> parameterProviders, Class<? extends Annotation> scopeAnnotation, Annotation annotation) {
        this.key = key;
        this.scopeAnnotation = scopeAnnotation;
        this.instance = instance;
        this.dependencies = dependencies;
        this.method = method;
        this.parameterProviders = parameterProviders;
        this.exposed = method.isAnnotationPresent(Exposed.class);
        this.annotation = annotation;
    }

    @Override
    public Key<T> getKey() {
        return key;
    }

    @Override
    public Method getMethod() {
        return method;
    }

    // exposed for GIN
    public Object getInstance() {
        return instance;
    }

    @Override
    public Object getEnclosingInstance() {
        return instance;
    }

    @Override
    public Annotation getAnnotation() {
        return annotation;
    }

    public void configure(Binder binder) {
        binder = binder.withSource(method);

        if (scopeAnnotation != null) {
            binder.bind(key).toProvider(this).in(scopeAnnotation);
        } else {
            binder.bind(key).toProvider(this);
        }

        if (exposed) {
            // the cast is safe 'cause the only binder we have implements PrivateBinder. If there's a
            // misplaced @Exposed, calling this will add an error to the binder's error queue
            ((PrivateBinder) binder).expose(key);
        }
    }

    @Override
    public T get() {
        Object[] parameters = new Object[parameterProviders.size()];
        for (int i = 0; i < parameters.length; i++) {
            parameters[i] = parameterProviders.get(i).get();
        }

        try {
            @SuppressWarnings({ "unchecked", "UnnecessaryLocalVariable" })
            T result = (T) doProvision(parameters);
            return result;
        } catch (IllegalAccessException e) {
            throw new AssertionError(e);
        } catch (InvocationTargetException e) {
            throw Exceptions.rethrowCause(e);
        }
    }

    /** Extension point for our subclasses to implement the provisioning strategy. */
    abstract Object doProvision(Object[] parameters) throws IllegalAccessException, InvocationTargetException;

    @Override
    public Set<Dependency<?>> getDependencies() {
        return dependencies;
    }

    @Override
    @SuppressWarnings("unchecked")
    public <B, V> V acceptExtensionVisitor(BindingTargetVisitor<B, V> visitor, ProviderInstanceBinding<? extends B> binding) {
        if (visitor instanceof ProvidesMethodTargetVisitor) {
            return ((ProvidesMethodTargetVisitor<T, V>) visitor).visit(this);
        }
        return visitor.visit(binding);
    }

    @Override
    public String toString() {
        String annotationString = annotation.toString();
        // Show @Provides w/o the com.google.inject prefix.
        if (annotation.annotationType() == Provides.class) {
            annotationString = "@Provides";
        } else if (annotationString.endsWith("()")) {
            // Remove the common "()" suffix if there are no values.
            annotationString = annotationString.substring(0, annotationString.length() - 2);
        }
        return annotationString + " " + StackTraceElements.forMember(method);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof ProviderMethod) {
            ProviderMethod<?> o = (ProviderMethod<?>) obj;
            return method.equals(o.method) && instance.equals(o.instance) && annotation.equals(o.annotation);
        } else {
            return false;
        }
    }

    @Override
    public int hashCode() {
        // Avoid calling hashCode on 'instance', which is a user-object
        // that might not be expecting it.
        // (We need to call equals, so we do.  But we can avoid hashCode.)
        return Objects.hashCode(method, annotation);
    }

    /*if[AOP]*/
    /**
     * A {@link ProviderMethod} implementation that uses {@link net.sf.cglib.reflect.FastClass#invoke}
     * to invoke the provider method.
     */
    private static final class FastClassProviderMethod<T> extends ProviderMethod<T> {
        final net.sf.cglib.reflect.FastClass fastClass;
        final int methodIndex;

        FastClassProviderMethod(Key<T> key, Method method, Object instance, ImmutableSet<Dependency<?>> dependencies, List<Provider<?>> parameterProviders, Class<? extends Annotation> scopeAnnotation, Annotation annotation) {
            super(key, method, instance, dependencies, parameterProviders, scopeAnnotation, annotation);
            // We need to generate a FastClass for the method's class, not the object's class.
            this.fastClass = BytecodeGen.newFastClass(method.getDeclaringClass(), Visibility.forMember(method));
            // Use the Signature overload of getIndex because it properly uses return types to identify
            // particular methods.  This is normally irrelevant, except in the case of covariant overrides
            // which java implements with a compiler generated bridge method to implement the override.
            this.methodIndex = fastClass.getIndex(new net.sf.cglib.core.Signature(method.getName(), org.objectweb.asm.Type.getMethodDescriptor(method)));
            Preconditions.checkArgument(this.methodIndex >= 0, "Could not find method %s in fast class for class %s", method, method.getDeclaringClass());
        }

        @Override
        public Object doProvision(Object[] parameters) throws IllegalAccessException, InvocationTargetException {
            return fastClass.invoke(methodIndex, instance, parameters);
        }
    }
    /*end[AOP]*/

    /**
     * A {@link ProviderMethod} implementation that invokes the method using normal java reflection. 
     */
    private static final class ReflectionProviderMethod<T> extends ProviderMethod<T> {
        ReflectionProviderMethod(Key<T> key, Method method, Object instance, ImmutableSet<Dependency<?>> dependencies, List<Provider<?>> parameterProviders, Class<? extends Annotation> scopeAnnotation, Annotation annotation) {
            super(key, method, instance, dependencies, parameterProviders, scopeAnnotation, annotation);
        }

        @Override
        Object doProvision(Object[] parameters) throws IllegalAccessException, InvocationTargetException {
            return method.invoke(instance, parameters);
        }
    }
}
